Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Sort Order Option setting to address #27759 #96214

Closed
wants to merge 62 commits into from

Conversation

leilapearson
Copy link
Contributor

This PR fixes #27759 by adding a Sort Order Option setting which can be found just below the existing Sort Order setting under Features -> Explorer .

To make it easier to see the effects of the change and the new settings, I created this repo:

The results of applying the old code as well as the new code with all combinations of Sort Order Option settings can be found here:

For the most part, sorts using the Sort Order Option default value of numeric will behave like sorts did before this option existed - but there are a few intentional changes. These are noted in the spreadsheet above, but here is the summary:

  • The old numeric sort did not compare names separately from extensions. This caused aggregate_repo.go to sort before aggregate.go because when using a locale-based sort the underscore character sorts before all alphabetic characters. By comparing names first and then extensions, the longer name will sort after the shorter name.

  • The old numeric sort used base sensitivity - but then had a check to see if the strings were unequal and resolved differences using a unicode sort order. This meant that case differences and accent differences were being taken into account, but according to unicode order, not locale order, which should not have been the case. The new code leaves the sensitivity setting alone (which normally defaults to variant) so that things will sort properly according to platform locale - which means it checks first if strings differ in base characters, then if they differ in character accents, and finally if they differ in case, and sorts based on what users would expect.

  • The old code treated any dotfile that had a leading dot and no other dots in the name as if it was an empty name plus an extension. It treated a dotfile with a leading dot and one or more other dots as if it was a name that started with a dot plus an extension. The new code treats the whole dotfile name as a name that starts with a dot and doesn't have an extension at all. This old behavior caused dotfiles that didn't have a second dot to appear between groups of files sorted by type - since the dotfiles themselves were treated as types. This seemed odd and inconsistent. For example, .eslintrc.json would sort with other .json files in the j section, which seemed like potentially a good thing - but .eslintignore would sort in between .cs files and .fs files - far from .eslintrc.json in the j extensions section. People are more used to seeing all dotfiles together, regardless of extension, so that's what the new code does.

There were quite a few decisions to make in how the various sort algorithms should behave - so I wound up tweaking things a number of times before I settled on the above behavior. I tried to follow the principle of least surprise when choosing between ways of handling things. Hopefully I succeeded, but if not, further tweaks can certainly be made.

CC: @isidorn

@msftclas
Copy link

msftclas commented Apr 27, 2020

CLA assistant check
All CLA requirements met.


// A collator with numeric sorting enabled.
const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => {
const collator = new Intl.Collator(undefined, { numeric: true });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the sensitivity: base option here. It was doing more harm than good since the code was falling back to a unicode comparison when there were accent or case differences.

}

return result;
// Using the numeric option in the collator will make compare(`foo1`, `foo01`) === 0. Sort the shorter name first.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old code sorted in unicode order here - and not just if the lengths were different - if there was any kind of difference between the strings including case or accent.

return oneName.length < otherName.length ? -1 : 1;
}

// Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extensions are a case where it seems best to use a case-insensitive (but not accent-insensitive) check before disambiguating numbers. Their case only comes into play at the very end if everything else has no difference. This makes sense because .MD and .md are treated as the same thing when determining file type.

return [(match && match[1]) || '', (match && match[3]) || ''];
let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || ''];

// treat an empty filename with an extension, or a filename that starts with a dot, as a dotfile name
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this special handling of dotfiles as mentioned in the PR submission comment.

* 0 otherwise
* ```
*/
export function compareCaseLowerFirst(one: string, other: string): number {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I exported these functions so I could test them. They seem simple, but this is actually one of the main areas that I tweaked. I originally wasn't sure whether or not uppercase and lowercase should be considered greater than non-case letters or not, and whether a word that starts with a non-case character like an underscore should be non-case because of that underscore, or should be judged by the first character that has a case.

I can remove the exports and the tests now if desired.

// Using the numeric option in the collator will
// make compare(`foo1`, `foo01`) === 0. We must disambiguate.
if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && a !== b) {
return a < b ? -1 : 1;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the unicode comparison here.

export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number {
const a = one || '';
const b = other || '';
const result = intlFileNameCollator.getValue().collator.compare(a, b);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that unlike the other comparisons, this one wasn't separately comparing names and extensions - which cause problems like the aggregate_repo.go vs aggregate.go issue.

}

const FileNameMatch = /^(.*?)(\.([^.]*))?$/;
Copy link
Contributor Author

@leilapearson leilapearson Apr 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't change this. I just moved it down next to the function that uses it.


if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && oneName !== otherName) {
return oneName < otherName ? -1 : 1;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note the unicode comparison here.

}

function extractNameAndExtension(str?: string | null): [string, string] {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... this one is exported in my code too? I'll remove the export.

return one < other ? -1 : 1;
}

/** Compares filenames by name unicode value, then extension unicode value. */
Copy link
Contributor Author

@leilapearson leilapearson Apr 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't seem to be used anywhere except in extHostDocumentData.test.ts. Could it possibly be removed? Or does it need to stick around? Same with compareByPrefix...

});

test('compareCaseLowerFirst', () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove these tests and not export these functions if that's preferred. The compareCase* functions are helper functions. It was good to be able to test them directly during development, but the other tests cover their behavior indirectly.

@isidorn
Copy link
Contributor

isidorn commented Apr 27, 2020

Thanks for the PR.
This week we are in endgame week which means we are not accepting any large changes, thus assigning this to May and will probably review this in the next two weeks.

Originaly I thought that this PR would only touch changes on the Explorer, however I see there are a lot of changes in the Comparer.ts thus also assigning @bpasero and @jrieken to take a look.

@isidorn isidorn added this to the May 2020 milestone Apr 27, 2020
@jrieken jrieken removed their assignment Apr 27, 2020
Copy link
Member

@bpasero bpasero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes in comparers.ts are beyond of what I can review just now. I am a bit worried that this change impacts more than just the explorer (e.g. we use comparers in quick open). I would opt for a solution that keeps the comparers as they work today for anything that is not the explorer where we want to provide the new search options.

@@ -398,6 +398,19 @@ configurationRegistry.registerConfiguration({
],
'description': nls.localize('sortOrder', "Controls sorting order of files and folders in the explorer.")
},
'explorer.sortOrderOption': {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it possibly to understand the difference between explorer.sortOrder setting and explorer.sortOrderOption?

Copy link
Contributor Author

@leilapearson leilapearson Apr 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort Order Option further refines the sort order. I was hoping the description made that clear - but can certainly reword if it doesn't.

image

Maybe "further refines" would be better wording.

// Export compareFileNamesNumeric by its old name too for backwards compatibility.
// Also keeping a placeholder parameter for the same reason. The parameter was sometimes supplied but was
// not used in the original code.
export { compareFileNamesNumeric as compareFileNames };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we seem to be supporting the old name, why are there changes in this PR to change to compareFileNamesNumeric?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really want to support the old name - but I wasn't sure how to safely change it? If I don't support the old name there are problems in CI building the stand-alone editor.

I've updated the code so that instead of just exporting an alias (which didn't work with tree-shaking) I export a compareFileNames function that calls compareFileNamesNumeric - so now the CI checks pass at least.

@bpasero bpasero removed their assignment Apr 27, 2020
@leilapearson
Copy link
Contributor Author

Thanks for the responses @isidorn and @bpasero. Totally understand waiting until May for the full review. It's a bigger change then I was expecting it to be too!

@bpasero I'm totally open to making the old functions - compareFileNames and compareFileExtensions - behave the same way as they did before - and using those functions outside of Explorer if that works best.

Once you do have a chance to review things you can let me know if you think any of the changes I made for compareFileNamesNumeric and compareFileExtensionsNumeric should be ported into compareFileNames and compareFileExtensions or not.

@leilapearson
Copy link
Contributor Author

P.S. I merged the latest changes from master into my fork and into my PR branch - which may not have been the best idea during the review. Just let me know if you'd like me to do something about that - and if so what's the best way to handle it. I did read through your coding guidelines but I'm not too familiar with your preferred workflow. Thanks! :-)

@isidorn
Copy link
Contributor

isidorn commented May 6, 2020

@leilapearson thanks again for this PR. I see you put a lot of work and effort in this PR and we really appreciate it. However there are a couple of issues for us to proceed further with this:

  1. I think you rebased and not merged with latest from master - thus the PR shows it changed 107 files. You should only merge, so the PR changes just the files you touched
  2. As already mentioned this PR touches some areas which affect the whole workbench. It would be great if this PR was more localised and only touching the explorer area
  3. It is much easier for us to review and merge smaller and concise PRs, I am not sure if it is possible to break this one in logical pieces which make sense seperatly.

Thank you again and I appologise if I did not specify this before you started on the implementation.

@isidorn isidorn modified the milestones: May 2020, Backlog May 6, 2020
@leilapearson
Copy link
Contributor Author

leilapearson commented May 6, 2020

Thanks for getting back to me @isidorn.

  1. I think you rebased and not merged with latest from master - thus the PR shows it changed 107 files. You should only merge, so the PR changes just the files you touched
  2. As already mentioned this PR touches some areas which affect the whole workbench. It would be great if this PR was more localised and only touching the explorer area
  3. It is much easier for us to review and merge smaller and concise PRs, I am not sure if it is possible to break this one in logical pieces which make sense separately.
  1. Yeah. I messed it up. Since you've asked for a smaller PR anyway, I'll close this PR up and submit new PRs with clean histories. See (3).

  2. Most of the changes in this PR are due to the new compare functions. Those functions aren't used outside of explorer. However, I did fix a couple of edge cases in the existing numeric sort and updated code outside of explorer to get these fixes too. Since that's a concern, I'll go ahead and change it so that the old functionality is used outside of explorer. If you later decide that these fixes would be good for other parts of workbench too, I can either update the PR or submit another new PR with that change.

  3. I have a couple of ideas on how to break this down into smaller PRs. Can you let me know which you prefer?
    a. One PR for the numeric sort algorithm fixes + one PR to add the new sort order options.
    b. One PR for the numeric sort algorithm fixes + one PR for each of the new options (upper, lower, mixed, and unicode) - so 5 PRs in total.

Since both options include submitting a PR with just the numeric sort algorithm fixes, I'll go ahead with that part while awaiting your response.

Thanks again @isidorn for the continued support and guidance. I did put a lot of effort into this but I don't at all mind putting in some more to make this easier to review.

@isidorn
Copy link
Contributor

isidorn commented May 7, 2020

@leilapearson let's go with 3a. Thanks!

@leilapearson
Copy link
Contributor Author

Will do @isidorn. Thanks!

@leilapearson
Copy link
Contributor Author

Closing this PR. Per our discussion, I'm splitting this PR into two separate PRs for easier review:

  • one PR for the numeric sort algorithm fixes + one PR to add the new sort order options.

I've already created PR #97200 for the first part. I will create the second PR soon.

@github-actions github-actions bot locked and limited conversation to collaborators Jun 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Align explorer sorting with platform sorting